package org.dcarew.stripjar;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipException;

public class Stripper {
  private File inFile;
  private File outFile;
  private File reorderFile;
  private Pattern noStripPattern;

  public Stripper(File inFile, File outFile) {
    this(inFile, outFile, null);
  }

  public Stripper(File inFile, File outFile, File reorderFile) {
    this.inFile = inFile;
    this.outFile = outFile;
    this.reorderFile = reorderFile;
  }

  public void setNoStrip(String strRegex) {
    if (strRegex != null) {
      noStripPattern = Pattern.compile(strRegex);
    } else {
      noStripPattern = null;
    }
  }

  public void strip() throws IOException {
    System.out.println("processing " + inFile + "...");

    List<String> reorderList = new ArrayList<String>();

    if (reorderFile != null) {
      reorderList.add("META-INF/MANIFEST.MF");
      reorderList.addAll(VerboseOutputFile.parseClasses(reorderFile));
    }

    ZipFile zipFile = new ZipFile(inFile);

    Properties manifest = parseProperties(zipFile.getInputStream(zipFile.getEntry("META-INF/MANIFEST.MF")));

    String mainClass = manifest.getProperty("Main-Class");

    System.out.println("  main class: " + mainClass);

    ClassEntriesCollector collector = new ClassEntriesCollector(zipFile, mainClass);

    if (noStripPattern != null) {
      addAdditionalNoStripClasses(zipFile, collector, noStripPattern);
    }

    collector.collect();

    ZipArchiveOutputStream zout = new ZipArchiveOutputStream(outFile);
    //zout.setComment("processed by stripjar");

    Enumeration<ZipArchiveEntry> entries = zipFile.getEntriesInPhysicalOrder();
    byte[] buffer = new byte[4096];

    int skipCount = 0;

    List<String> reorderAdded = new ArrayList<String>();

    for (String reorderName : reorderList) {
      ZipArchiveEntry entry = zipFile.getEntry(reorderName);

      if (entry != null) {
        if (!isClassEntry(entry) || collector.contains(entry)) {
          ZipArchiveEntry zentry = new ZipArchiveEntry(entry.getName());

          zentry.setSize(entry.getSize());
          zentry.setTime(entry.getTime());
          zout.putArchiveEntry(zentry);
          zout.write(readFully(zipFile.getInputStream(entry), buffer));

          zout.closeArchiveEntry();

          reorderAdded.add(reorderName);
        }
      }
    }

    while (entries.hasMoreElements()) {
      ZipArchiveEntry entry = entries.nextElement();

      if (!reorderAdded.contains(entry.getName())) {
        if (!isClassEntry(entry) || collector.contains(entry)) {
          ZipArchiveEntry zentry = new ZipArchiveEntry(entry.getName());

          zentry.setSize(entry.getSize());
          zentry.setTime(entry.getTime());
          zout.putArchiveEntry(zentry);
          zout.write(readFully(zipFile.getInputStream(entry), buffer));

          zout.closeArchiveEntry();
        } else {
          skipCount++;
        }
      }
    }

    zout.close();

    double shrinkFactor = ((inFile.length() - outFile.length()) / (double) inFile.length()) * 100.0;

    System.out.print(String.format("  %,d classes stripped", skipCount));
    if (reorderAdded.size() > 0) {
      System.out.print(String.format(", %,d reordered", reorderAdded.size()));
    }
    System.out.println();

    System.out.println(String.format(
        "wrote %s [%,dkb ==> %,dkb, shrunk by %.2f%%]",
        outFile,
        toKb(inFile.length()),
        toKb(outFile.length()),
        shrinkFactor));
  }

  private void addAdditionalNoStripClasses(ZipFile zipFile, ClassEntriesCollector collector,
      Pattern noStripPattern) {
    Enumeration<ZipArchiveEntry> entries = zipFile.getEntriesInPhysicalOrder();

    while (entries.hasMoreElements()) {
      ZipArchiveEntry entry = entries.nextElement();

      if (isClassEntry(entry)) {
        String pathName = entry.getName();

        String className = Utils.pathNameToClassName(pathName);

        Matcher matcher = noStripPattern.matcher(className);

        if (matcher.matches()) {
          collector.addToWhitelist(className);
        }
      }
    }
  }

  private boolean isClassEntry(ZipArchiveEntry entry) {
    return !entry.isDirectory() && entry.getName().endsWith(".class");
  }

  private Properties parseProperties(InputStream in) throws ZipException, IOException {
    Properties props = new Properties();
    props.load(in);
    return props;
  }

  private byte[] readFully(InputStream in, byte[] buffer) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();

    int count = in.read(buffer);

    while (count != -1) {
      out.write(buffer, 0, count);
      count = in.read(buffer);
    }

    return out.toByteArray();
  }

  private long toKb(long l) {
    return (l + 1023) / 1024;
  }

}
